 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.texteditor;

 import java.util.ArrayList ;
 import java.util.Iterator ;
 import java.util.List ;

 import org.osgi.framework.Bundle;

 import org.eclipse.core.resources.IMarker;
 import org.eclipse.core.resources.IResourceStatus;

 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IConfigurationElement;
 import org.eclipse.core.runtime.IExtensionPoint;
 import org.eclipse.core.runtime.ILog;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.Status;

 import org.eclipse.core.filebuffers.IPersistableAnnotationModel;

 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.Position;
 import org.eclipse.jface.text.source.Annotation;
 import org.eclipse.jface.text.source.AnnotationModel;
 import org.eclipse.jface.text.source.IAnnotationMap;

 import org.eclipse.ui.editors.text.EditorsUI;

 import org.eclipse.ui.PlatformUI;


 /**
  * Abstract implementation of a marker-based annotation model.
  * <p>
  * Markers are provided by an underlying source (a subclass responsibility).
  * Markers whose textual range gets deleted during text editing are removed
  * from the model on save. The {@link #updateMarkers(IDocument)} method can be used
  * to force the model to update the source's markers with any changes to their
  * locations due to edits. Clients can register a {@link org.eclipse.ui.texteditor.IMarkerUpdater}
  * objects in order to define the process of marker updating. Registration can be done
  * using the <code>"org.eclipse.ui.markerUpdaters"</code> extension point.
  * </p>
  * <p>
  * Subclasses must implement the following methods:
  * <ul>
  * <li><code>retrieveMarkers</code></li>
  * <li><code>isAcceptable</code></li>
  * <li><code>deleteMarkers</code></li>
  * <li><code>listenToMarkerChanges</code></li>
  * </ul>
  * </p>
  */
 public abstract class AbstractMarkerAnnotationModel extends AnnotationModel implements IPersistableAnnotationModel {

     /** List of annotations whose text range became invalid because of document changes */
     private List fDeletedAnnotations= new ArrayList (2);
     /** List of registered and instantiated marker updaters */
     private List fInstantiatedMarkerUpdaters= null;
     /** List of registered but not yet instantiated marker updaters */
     private List fMarkerUpdaterSpecifications= null;


     /**
      * Retrieves all markers from this model.
      * <p>
      * Subclasses must implement this method.</p>
      *
      * @return the list of markers
      * @throws CoreException if there is a problem getting the markers
      */
     protected abstract IMarker[] retrieveMarkers() throws CoreException;

     /**
      * Deletes the given markers from this model.
      * <p>
      * Subclasses must implement this method.</p>
      *
      * @param markers the array of markers
      * @throws CoreException if there are problems deleting the markers
      */
     protected abstract void deleteMarkers(IMarker[] markers) throws CoreException;

     /**
      * Tells the model whether it should listen for marker changes.
      * <p>
      * Subclasses must implement this method.</p>
      *
      * @param listen <code>true</code> if this model should listen, and
      * <code>false</code> otherwise
      */
     protected abstract void listenToMarkerChanges(boolean listen);

     /**
      * Determines whether the marker is acceptable as an addition to this model.
      * If the marker, say, represents an aspect or range of no interest to this
      * model, the marker is rejected.
      * <p>
      * Subclasses must implement this method.</p>
      *
      * @param marker the marker
      * @return <code>true</code> if the marker is acceptable
      */
     protected abstract boolean isAcceptable(IMarker marker);

     /**
      * Creates a new annotation model. The annotation model does not manage any
      * annotations and is not connected to any document.
      */
     protected AbstractMarkerAnnotationModel() {
     }

     /**
      * Adds the given marker updater to this annotation model.
      * It is the client's responsibility to ensure the consistency
      * of the set of registered marker updaters.
      *
      * @param markerUpdater the marker updater to be added
      */
     protected void addMarkerUpdater(IMarkerUpdater markerUpdater) {
         if (!fInstantiatedMarkerUpdaters.contains(markerUpdater))
             fInstantiatedMarkerUpdaters.add(markerUpdater);
     }

     /**
      * Removes the given marker updater from this annotation model.
      *
      * @param markerUpdater the marker updater to be removed
      */
     protected void removeMarkerUpdater(IMarkerUpdater markerUpdater) {
         fInstantiatedMarkerUpdaters.remove(markerUpdater);
     }

     /**
      * Creates a new annotation for the given marker.
      * <p>
      * Subclasses may override.</p>
      *
      * @param marker the marker
      * @return the new marker annotation
      */
     protected MarkerAnnotation createMarkerAnnotation(IMarker marker) {
         return new MarkerAnnotation(marker);
     }

     /**
      * Handles an unanticipated <code>CoreException</code> in
      * a standard manner.
      *
      * @param exception the exception
      * @param message a message to aid debugging
      */
     protected void handleCoreException(CoreException exception, String message) {

         Bundle bundle = Platform.getBundle(PlatformUI.PLUGIN_ID);
         ILog log= Platform.getLog(bundle);
         if (message != null)
             log.log(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, message, exception));
         else
             log.log(exception.getStatus());
     }

     /**
      * Creates and returns the character position of the given marker based
      * on its attributes.
      * <p>
      * Subclasses may override.</p>
      *
      * @param marker the marker
      * @return the new position or <code>null</code> if the marker attributes do not specify a valid position
      */
     protected Position createPositionFromMarker(IMarker marker) {

         int start= MarkerUtilities.getCharStart(marker);
         int end= MarkerUtilities.getCharEnd(marker);

         if (start > end) {
             end= start + end;
             start= end - start;
             end= end - start;
         }

         if (start == -1 && end == -1) {
             // marker line number is 1-based
 int line= MarkerUtilities.getLineNumber(marker);
             if (line > 0 && fDocument != null) {
                 try {
                     start= fDocument.getLineOffset(line - 1);
                     end= start;
                 } catch (BadLocationException x) {
                 }
             }
         }

         if (start > -1 && end > -1)
             return new Position(start, end - start);

         return null;
     }

     /**
      * Creates an annotation for the given marker and adds it to this model.
      * Does nothing if the marker is not acceptable to this model.
      *
      * @param marker the marker
      * @see #isAcceptable(IMarker)
      */
     protected final void addMarkerAnnotation(IMarker marker) {

         if (isAcceptable(marker)) {
             Position p= createPositionFromMarker(marker);
             if (p != null)
                 try {
                     MarkerAnnotation annotation= createMarkerAnnotation(marker);
                     if (annotation != null)
                         addAnnotation(annotation, p, false);
                 } catch (BadLocationException e) {
                     // ignore invalid position
 }
         }
     }

     /**
      * Connects to the source of markers as marker change listener.
      * @see AnnotationModel#connected()
      */
     protected void connected() {

         listenToMarkerChanges(true);

         try {
             catchupWithMarkers();
         } catch (CoreException x) {
             if (x.getStatus().getCode() != IResourceStatus.RESOURCE_NOT_FOUND)
                 handleCoreException(x, TextEditorMessages.AbstractMarkerAnnotationModel_connected);
         }

         fireModelChanged();
     }

     /**
      * Installs all marker updaters for this marker annotation model.
      */
     private void installMarkerUpdaters() {

         // initialize lists - indicates that the initialization happened
 fMarkerUpdaterSpecifications= new ArrayList (2);
         fInstantiatedMarkerUpdaters= new ArrayList (2);

         // populate list
 IExtensionPoint extensionPoint= Platform.getExtensionRegistry().getExtensionPoint(EditorsUI.PLUGIN_ID, "markerUpdaters"); //$NON-NLS-1$
 if (extensionPoint != null) {
             IConfigurationElement[] elements= extensionPoint.getConfigurationElements();
             for (int i= 0; i < elements.length; i++)
                 fMarkerUpdaterSpecifications.add(elements[i]);
         }
     }

     /**
      * Uninstalls all marker updaters.
      */
     private void uninstallMarkerUpdaters() {
         if (fInstantiatedMarkerUpdaters != null) {
             fInstantiatedMarkerUpdaters.clear();
             fInstantiatedMarkerUpdaters= null;
         }

         if (fMarkerUpdaterSpecifications != null) {
             fMarkerUpdaterSpecifications.clear();
             fMarkerUpdaterSpecifications= null;
         }
     }

     /**
      * Removes the marker change listener.
      * @see AnnotationModel#disconnected()
      */
     protected void disconnected() {
         listenToMarkerChanges(false);
         uninstallMarkerUpdaters();
     }

     /**
      * Returns the position known to this annotation model for the given marker.
      *
      * @param marker the marker
      * @return the position, or <code>null</code> if none
      */
     public Position getMarkerPosition(IMarker marker) {
         MarkerAnnotation a= getMarkerAnnotation(marker);
         if (a != null) {
             return (Position) getAnnotationMap().get(a);
         }
         return null;
     }

     /**
      * Updates the annotation corresponding to the given marker which has changed
      * in some way.
      * <p>
      * Subclasses may override.</p>
      *
      * @param marker the marker
      */
     protected void modifyMarkerAnnotation(IMarker marker) {
         MarkerAnnotation a= getMarkerAnnotation(marker);
         if (a != null) {
             Position p= createPositionFromMarker(marker);
             if (p != null) {
                 a.update();
                 modifyAnnotationPosition(a, p, false);
             }
         } else
             addMarkerAnnotation(marker);
     }

     /*
      * @see AnnotationModel#removeAnnotations(List, boolean, boolean)
      */
     protected void removeAnnotations(List annotations, boolean fireModelChanged, boolean modelInitiated) {
         if (annotations != null && annotations.size() > 0) {

             List markerAnnotations= new ArrayList ();
             for (Iterator e= annotations.iterator(); e.hasNext();) {
                 Annotation a= (Annotation) e.next();
                 if (a instanceof MarkerAnnotation)
                     markerAnnotations.add(a);

                 // remove annotations from annotation model
 removeAnnotation(a, false);
             }

             if (markerAnnotations.size() > 0) {

                 if (modelInitiated) {
                     // if model initiated also remove it from the marker manager

                     listenToMarkerChanges(false);
                     try {

                         IMarker[] m= new IMarker[markerAnnotations.size()];
                         for (int i= 0; i < m.length; i++) {
                             MarkerAnnotation ma = (MarkerAnnotation) markerAnnotations.get(i);
                             m[i]= ma.getMarker();
                         }
                         deleteMarkers(m);

                     } catch (CoreException x) {
                         handleCoreException(x, TextEditorMessages.AbstractMarkerAnnotationModel_removeAnnotations);
                     }
                     listenToMarkerChanges(true);

                 } else {
                     // remember deleted annotations in order to remove their markers later on
 fDeletedAnnotations.addAll(markerAnnotations);
                 }
             }

             if (fireModelChanged)
                 fireModelChanged();
         }
     }

     /**
      * Removes the annotation corresponding to the given marker. Does nothing
      * if there is no annotation for this marker.
      *
      * @param marker the marker
      */
     protected final void removeMarkerAnnotation(IMarker marker) {
         MarkerAnnotation a= getMarkerAnnotation(marker);
         if (a != null) {
             removeAnnotation(a, false);
         }
     }

     /**
      * Re-populates this model with annotations for all markers retrieved
      * from the maker source via <code>retrieveMarkers</code>.
      *
      * @throws CoreException if there is a problem getting the markers
      */
     private void catchupWithMarkers() throws CoreException {

         for (Iterator e=getAnnotationIterator(false); e.hasNext();) {
             Annotation a= (Annotation) e.next();
             if (a instanceof MarkerAnnotation)
                 removeAnnotation(a, false);
         }

         IMarker[] markers= retrieveMarkers();
         if (markers != null) {
             for (int i= 0; i < markers.length; i++)
                 addMarkerAnnotation(markers[i]);
         }
     }

     /**
      * Returns this model's annotation for the given marker.
      *
      * @param marker the marker
      * @return the annotation, or <code>null</code> if none
      */
     public final MarkerAnnotation getMarkerAnnotation(IMarker marker) {
         Iterator e= getAnnotationIterator(false);
         while (e.hasNext()) {
             Object o= e.next();
             if (o instanceof MarkerAnnotation) {
                 MarkerAnnotation a= (MarkerAnnotation) o;
                 if (marker.equals(a.getMarker())) {
                     return a;
                 }
             }
         }
         return null;
     }

     /**
      * Creates a marker updater as specified in the given configuration element.
      *
      * @param element the configuration element
      * @return the created marker updater or <code>null</code> if none could be created
      */
     private IMarkerUpdater createMarkerUpdater(IConfigurationElement element) {
         try {
             return (IMarkerUpdater) element.createExecutableExtension("class"); //$NON-NLS-1$
 } catch (CoreException x) {
             handleCoreException(x, TextEditorMessages.AbstractMarkerAnnotationModel_createMarkerUpdater);
         }

         return null;
     }

     /**
      * Checks whether a marker updater is registered for the type of the
      * given marker but not yet instantiated. If so, the method instantiates
      * the marker updater and registers it with this model.
      *
      * @param marker the marker for which to look for an updater
      * @since 2.0
      */
     private void checkMarkerUpdaters(IMarker marker) {
         List toBeDeleted= new ArrayList ();
         for (int i= 0; i < fMarkerUpdaterSpecifications.size(); i++) {
             IConfigurationElement spec= (IConfigurationElement) fMarkerUpdaterSpecifications.get(i);
             String markerType= spec.getAttribute("markerType"); //$NON-NLS-1$
 if (markerType == null || MarkerUtilities.isMarkerType(marker, markerType)) {
                 toBeDeleted.add(spec);
                 IMarkerUpdater updater= createMarkerUpdater(spec);
                 if (updater != null)
                     addMarkerUpdater(updater);
             }
         }

         for (int i= 0; i < toBeDeleted.size(); i++)
             fMarkerUpdaterSpecifications.remove(toBeDeleted.get(i));
     }

     /**
      * Updates the given marker according to the given position in the given
      * document. If the given position is <code>null</code>, the marker is
      * assumed to carry the correct positional information. If it is detected
      * that the marker is invalid and should thus be deleted, this method
      * returns <code>false</code>.
      * <p>
      * <strong>Note:</strong> This implementation queries the registered
      * {@linkplain IMarkerUpdater}s. If any of these updaters returns
      * <code>false</code> this method also returns <code>false</code>.
      * </p>
      *
      * @param marker the marker to be updated
      * @param document the document into which the given position points
      * @param position the current position of the marker inside the given document
      * @return <code>false</code> if the marker is invalid
      * @throws CoreException if there is a problem updating the marker
      * @since 2.0
      * @deprecated use <code>updateMarker(IDocument, IMarker, Position)</code> instead. This method will be changed to protected.
      */
     public boolean updateMarker(IMarker marker, IDocument document, Position position) throws CoreException {

         if (fMarkerUpdaterSpecifications == null)
             installMarkerUpdaters();

         if (!fMarkerUpdaterSpecifications.isEmpty())
             checkMarkerUpdaters(marker);

         boolean isOK= true;

         for (int i= 0; i < fInstantiatedMarkerUpdaters.size(); i++) {
             IMarkerUpdater updater= (IMarkerUpdater) fInstantiatedMarkerUpdaters.get(i);
             String markerType= updater.getMarkerType();
             if (markerType == null || MarkerUtilities.isMarkerType(marker, markerType)) {

                 if (position == null) {
                     /* compatibility code */
                     position= createPositionFromMarker(marker);
                 }

                 isOK= (isOK && updater.updateMarker(marker, document, position));
             }
         }

         return isOK;
     }

     /**
      * Updates the given marker according to the given position in the given
      * document. If the given position is <code>null</code>, the marker is
      * assumed to carry the correct positional information. If it is detected
      * that the marker is invalid and should thus be deleted, this method
      * returns <code>false</code>.
      *
      * @param marker the marker to be updated
      * @param document the document into which the given position points
      * @param position the current position of the marker inside the given document
      * @return <code>false</code> if the marker is invalid
      * @throws CoreException if there is a problem updating the marker
      * @since 3.0
      */
     public boolean updateMarker(IDocument document, IMarker marker, Position position) throws CoreException {
         listenToMarkerChanges(false);
         try {
             return updateMarker(marker, document, position);
         } finally {
             listenToMarkerChanges(true);
         }
     }

     /**
      * Updates the markers managed by this annotation model by calling
      * all registered marker updaters (<code>IMarkerUpdater</code>).
      *
      * @param document the document to which this model is currently connected
      * @throws CoreException if there is a problem updating the markers
      */
     public void updateMarkers(IDocument document) throws CoreException {

         Assert.isTrue(fDocument == document);

         IAnnotationMap annotationMap= getAnnotationMap();

         if (annotationMap.size() == 0 && fDeletedAnnotations.size() == 0)
             return;

         if (fMarkerUpdaterSpecifications == null)
             installMarkerUpdaters();

         listenToMarkerChanges(false);

         try {

             // update all markers with the positions known by the annotation model
 for (Iterator e= getAnnotationIterator(false); e.hasNext();) {
                 Object o= e.next();
                 if (o instanceof MarkerAnnotation) {
                     MarkerAnnotation a= (MarkerAnnotation) o;
                     IMarker marker= a.getMarker();
                     Position position= (Position) annotationMap.get(a);
                     if ( !updateMarker(marker, document, position)) {
                         if ( !fDeletedAnnotations.contains(a))
                             fDeletedAnnotations.add(a);
                     }
                 }
             }

             if (!fDeletedAnnotations.isEmpty()) {
                 removeAnnotations(fDeletedAnnotations, true, true);
                 fDeletedAnnotations.clear();
             }

         } finally {

             listenToMarkerChanges(true);

         }
     }

     /**
      * Resets all the markers to their original state.
      */
     public void resetMarkers() {

         // re-initializes the positions from the markers
 for (Iterator e= getAnnotationIterator(false); e.hasNext();) {
             Object o= e.next();
             if (o instanceof MarkerAnnotation) {
                 MarkerAnnotation a= (MarkerAnnotation) o;
                 Position p= createPositionFromMarker(a.getMarker());
                 if (p != null) {
                     removeAnnotation(a, false);
                     try {
                         addAnnotation(a, p, false);
                     } catch (BadLocationException e1) {
                         // ignore invalid position
 }
                 }
             }
         }

         // add the markers of deleted positions back to the annotation model
 for (Iterator e= fDeletedAnnotations.iterator(); e.hasNext();) {
             Object o= e.next();
             if (o instanceof MarkerAnnotation) {
                 MarkerAnnotation a= (MarkerAnnotation) o;
                 Position p= createPositionFromMarker(a.getMarker());
                 if (p != null)
                     try {
                         addAnnotation(a, p, false);
                     } catch (BadLocationException e1) {
                         // ignore invalid position
 }
             }
         }
         fDeletedAnnotations.clear();

         // fire annotation model changed
 fireModelChanged();
     }

     /*
      * @see org.eclipse.jface.text.source.IPersistableAnnotationModel#commit(org.eclipse.jface.text.IDocument)
      */
     public void commit(IDocument document) throws CoreException {
         updateMarkers(document);
     }

     /*
      * @see org.eclipse.jface.text.source.IPersistableAnnotationModel#revert(org.eclipse.jface.text.IDocument)
      */
     public void revert(IDocument document) {
         resetMarkers();
     }

     /*
      * @see org.eclipse.jface.text.source.IPersistableAnnotationModel#reinitialize(org.eclipse.jface.text.IDocument)
      */
     public void reinitialize(IDocument document) {
         resetMarkers();
     }
 }

